package org.krayne.swivel.list;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.swing.AbstractListModel;

import org.krayne.swivel.filter.SimpleStringFilter;
import org.krayne.swivel.filter.StringFilter;

/**
 * This {@code ListModel} provides string filtering on its list items.
 * The filtering is done on the items' {@code toString()} method.
 * The results returned by the {@code ListModel} methods like {@code getSize()}
 * and {@code getElementAt(int)} are all for the FILTERED list, not the
 * unfiltered list.
 * 
 * @author dhsu
 */
public class FilterListModel extends AbstractListModel {
    private static final long serialVersionUID = -191700265681629836L;
    
    private final List<Object> unfilteredElements;
    private final List<Object> filteredElements;
    private StringFilter filter;
    
    /**
     * Constructs a filter list model with an all-pass string filter.
     */
    public FilterListModel() {
        this(SimpleStringFilter.ALL_PASS);
    }
    
    /**
     * Constructs a filter list model with the specified string filter.
     * 
     * @param filter a string filter
     */
    public FilterListModel(StringFilter filter) {
        this.unfilteredElements = new ArrayList<Object>();
        this.filteredElements = new ArrayList<Object>();
        this.filter = filter;
    }

    /**
     * Gets a collection of the filtered elements (i.e. passed the filter)
     * 
     * @return filtered elements
     */
    public Collection<Object> getFilteredElements() {
        return Collections.unmodifiableCollection(filteredElements);
    }
    
    /**
     * Gets a collection of all elements.
     * 
     * @return unfiltered elements
     */
    public Collection<Object> getUnfilteredElements() {
        return Collections.unmodifiableCollection(unfilteredElements);
    }
    
    /**
     * Sets the string filter.
     * 
     * @param filter a string filter
     */
    public void setFilter(StringFilter filter) {
        if(!this.filter.equals(filter)) {
            this.filter = filter;
            this.buildFilteredElements();
        }
    }
    
    /**
     * Gets the model's string filter.
     * 
     * @return the model's string filter
     */
    public StringFilter getFilter() {
        return this.filter;
    }
    
    /**
     * Appends an element to the end of the list model.
     * 
     * @param element an element
     */
    public void addElement(Object element) {
        unfilteredElements.add(element);
        if(passesFilter(element.toString(), this.filter)) {
            int addIndex = filteredElements.size();
            filteredElements.add(element);
            fireIntervalAdded(this, addIndex, addIndex);
        }
    }
    
    /**
     * Removes the specified element from the model. If the element is not
     * found, then the method returns false.
     * 
     * @param element the element to remove
     * @return true if successfully removed, false if the list did not contain
     *         the specified element
     */
    public boolean removeElement(Object element) {
        int unfilteredIndex = unfilteredElements.indexOf(element);
        if(unfilteredIndex != -1) {
            unfilteredElements.remove(unfilteredIndex);
            
            // now attempt to remove the element from the filtered list
            int filteredIndex = filteredElements.indexOf(element);
            if(filteredIndex != -1) {
                filteredElements.remove(filteredIndex);
                fireIntervalRemoved(this, filteredIndex, filteredIndex);
            }
            return true;
        }
        return false;
    }
    
    /**
     * Removes all elements from the list model.
     */
    public void removeAllElements() {
        int lastRemovedIndex = filteredElements.size() - 1;
        unfilteredElements.clear();
        filteredElements.clear();
        fireIntervalRemoved(this, 0, lastRemovedIndex);
    }
    
    /**
     * Updates the filtered elements by iterating through all items
     * and applying the filter to each one.
     */
    private void buildFilteredElements() {
        filteredElements.clear();
        for(Object element : unfilteredElements) {
            if(passesFilter(element.toString(), this.filter)) {
                filteredElements.add(element);
            }
        }
        fireContentsChanged(this, 0, this.getSize());
    }
    
    /**
     * A helper method that does an additional null check on the string filter.
     * The method returns true if the specified string passes the specified
     * filter. If the filter is null, then the string automatically passes.
     * 
     * @param s a string
     * @param stringFilter a string filter
     * @return true if the string passes the filter
     */
    private static boolean passesFilter(String s, StringFilter stringFilter) {
        if(stringFilter == null) {
            return true;
        }
        return stringFilter.pass(s);
    }
    
    /**
     * {@inheritDoc}
     */
    public Object getElementAt(int index) {
        return getListElementAt(filteredElements, index);
    }
    
    /**
     * Gets the unfiltered element given an index.
     * 
     * @param index index into the unfiltered elements list
     * @return the element at the specified index, null if the index is out of
     *         bounds
     */
    public Object getUnfilteredElementAt(int index) {
        return getListElementAt(unfilteredElements, index);
    }
    
    /**
     * Gets the list element of the specified list with the specified index.
     * 
     * @param list a list
     * @param index an index
     * @return the list item in the list at the index, null if the index is out
     *         of bounds
     */
    private Object getListElementAt(List<Object> list, int index) {
        if(index < list.size() && index >= 0) {
            return list.get(index);
        }
        return null;
    }

    /**
     * Gets the number of elements that passed the filter.
     * 
     * @return the number of elements that passed the filter
     */
    public int getSize() {
        return filteredElements.size();
    }
    
    /**
     * Gets the total number of elements.
     * 
     * @return the total number of elements
     */
    public int getUnfilteredSize() {
        return unfilteredElements.size();
    }
}
